查看原文
其他

你知道source map如何帮你定位源码么?

若川视野 2022-05-01

The following article is from 前端这件小事 Author daisy

大家好,我是若川。今天分享一篇我们经常会忽略的定位原始代码位置原理的文章。文章不长,例子不错,可以先收藏,有空时动手试试。

学习源码系列年度总结JS基础系列



前言

我们知道,代码上线前要经过压缩,美化,混淆等步骤,真正上线之后的代码亲妈都不认识。这也可以理解,为了防止别人看到你的源码发现你的漏洞从而去攻击你的网页。

但问题是,如果自己的代码在线上跑出了bug,连自己都看不懂错在了哪里。这时候就需要代码还原工具来帮助我们还原一下代码,从而找到出错位置。

这个还原神器就是我们今天的主角source map。今天我们来聊聊它是怎么还原我们的代码的。

source map在哪

通常,我们用webpack的构建去生成代码的时候,可以去配置devtool 让它生成source map,这样在最后生成的dist就会找到.js.map的文件。

以这个list.js为例

const a = 111;
console.log(a);

生成的dist文件

有一行代码去引用了js.map文件,我们在打开这个文件,可以看到,生成的map文件长这样

{"version":3,"file":"vote/list/list.c1e192cf.js","sources":["webpack:///webpack/bootstrap","webpack:///./src/pages/vote/list/list.js"],"sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/mpres/zh_CN/htmledition/pages/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","var a = 111;\nconsole.log(a);"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;AClFA;AACA;;;;A","sourceRoot":""}

别看这段那么多,其实也就这几个字段:

{ "version": 3, // source map的版本 "file": "", // 转换后的文件名 "source": [], // 来源文件的代码 "names": [], // 转换前所有的变量和属性名 "mapping": "" // 记录位置信息的字符串}

这其中真正用于定位的就是这个mapping字段里的信息。

source map 是如何还原代码的

由于上面的例子过于复杂,这里我们用个简单的例子来说明一下。

源代码

/* 注释 */
var name = "abc";

压缩后的代码

var name="abc";
//# sourceMappingURL=a.js.map

对应的source-map

{
"version":3,
"sources":["a.js"],
"names":["name"],
"mappings":";AACA,IAAIA,KAAO",
"file":"a.js",
"sourcesContent":["/* 注释 */\nvar name = \"abc\";"]
}

接下来我们看看这个;AACA,IAAIA,KAAO在说什么。

其中分号;代表一个空行。逗号,代表一个位置。

AACA标明的是var的位置,它是先经过VLQ编码,在经过base64编码而成。VLQ跟base64不懂都没什么关系,我们这里知道它是一种编码方式即可。

下面举个栗子,来看看188经过VLQ 与 base64编码的过程及结果。

首先188的二进制表示是10111100,不能满足VLQ 6字节的要求,所以这里将它拆成两部分,在交换一下。

接着1100前面补1,因为后面还有一块block。结尾补0,因为188是一个正数。第一段最终转出来就是111000。

在看后面一段1011,我们只需要在前面补两个0。其中第一个0表示没有block在后面了,第二个0是因为不足5位左边补0。第二段最终转出来就是001011。

111000对应VLQ就是56,然后在对应base64的4。

而001011对应VLQ是11,在对应base64就是L。

AACA转回来就是逆方向操作。

有点麻烦,我估计你们也不想算,所以我们先用一个库vlq来帮忙转换一下它。

打印结果如下:

可以看到AACA解析出来是0010。其中,

第一位,表示这个位置在(转换后的代码的)的第几列。

第二位,表示这个位置属于sources属性中的哪一个文件。

第三位,表示这个位置属于转换前代码的第几行。

第四位,表示这个位置属于转换前代码的第几列。

这里有两点需要注意:

1、位置都是以0为基数算起。

2、计算的是相对与前一个位置的相对位置。

所以这个0010,这里就代表着var在压缩后代码的第0列,对应第0个源码文件的1行0列。

同理,第二个IAAIA转过来是4004,这个是相对于上一个字符的位置,所以我们需要加起来,也就是4014。说明name在压缩后代码的第4列,对应第0个源码文件的的第1行,第4列。

第三个KAAO转过来是5007,相加前面的也就是90111,说明abc是转换后的代码第9列,对应第0个源码文件的的第1行,第11列。

再来个栗子

这是转换前的scipt.js代码

这是编译后的代码scipt-transpiled.js

这个是source map 文件

这个mapping对应回转换后的代码就长这样:


大家可以自行分析一下这个例子 。


以上,就是今天分享的source map 所有内容。

参考文档:

https://medium.com/@trungutt/yet-another-explanation-on-sourcemap-669797e418ce



最近组建了一个江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 拉你进群。



点击方卡片关注我、加个星标

一个愿景是帮助5年内前端人成长的公众号可加我个人微信 ruochuan12,长期交流学习

推荐阅读


我在阿里招前端,该怎么帮你(可进面试群)2年前端经验,做的项目没技术含量,怎么办

················· 若川简介 ·················


你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》多篇,在知乎、掘金收获超百万阅读。

从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结

同时,活跃在知乎@若川,掘金@若川。致力于分享前端开发经验,愿景:帮助5年内前端人走向前列。

今日话题
我经常推荐学会使用技术完成开发的同时也要多要研究原理。其实就是不停留在只会使用的层面,重基础懂原理,知其然知其所以然。欢分享、收藏、点赞、在看我的公众号文章~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存